package web.dynamic.server;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.widget.form.FormPanel;
import com.extjs.gxt.ui.client.widget.grid.Grid;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;




/**
 * 
 * Dynamic property mapping in implemented using various components
 * 
 * 1 schema generator - in this case is a java class parser the reads properties from java classes using reflection
 * 2 Component Builder - is is a class that builds the runtime components by setting the properties of the component from the config
 * 3 Component Builder Generator - generates component builders from schema
 * 4 Config - a class that holds the property values, it is also the transport mechanism between client and server
 * 5 Context - the class that holds all the named instances at runtime
 * 6 Config persistence Schema - tables that components use to persist them selves, config tables (* components can be nested)
 * 7 Dynamic persistence api - bassically a dynamic sql crud interface, it generates the sql on the fly from the model config
 * 8 BPM api - persistence and interaction with workflows
 * 9 Action API - these are pieces of reusable code that make things happen  
 * 
 * @author michaelhancock
 *
 */

public class Handler extends HttpServlet {

	/**	
	 * 
	 */
	private static final long serialVersionUID = -8341848491832159812L;

	public String generateBuilder(List<Class<?>> types) {
		
		
		/*
		 * This whole section will need to be rewritten to support custom setters, in the code below there are a few, but it needs to be dynamic and extensible
		 * 
		 * there are a few nuiances, 
		 * 	the config properties do not have a one to one rel with setters ( a setter can require many config props (ie setSize(h,w))) 
		 *  setters might require aditional imports
		 *  
		 *  BindablePropertySchema
		 *  	String SetterGenerator 
		 *  	String PropertyType
		 *  	ConfigName - the config var name
		 *  	MethodName
		 * 
		 * interface SetterGenerator{
		 * 		String generateSetter( PropInfo )
		 * }
		 * 
		 * for each prop:
		 * 	sg = getGenerator( prop.setterGenerator)
		 *  body.append( sg.gen( prop ) ) 
		 */
		

		StringWriter imports = new StringWriter();
		StringWriter xtypes = new StringWriter();
		StringWriter methods = new StringWriter();

		String xtypeTpl = "if( xtype == \"{TYPE}\" ) {METHOD}(cfg);";
		boolean first = true;
		for (Class<?> c : types) {

			imports.append("import "+c.getCanonicalName()+";\n");
			String methodName = "build_" + c.getCanonicalName().replace(".", "_");
			xtypes.append(xtypeTpl.replace("{TYPE}", c.getCanonicalName())
					.replace("{METHOD}",methodName));

			if (first) {
				first = false;
				xtypeTpl = "\n\t\t\telse " + xtypeTpl;
			}
			
			StringWriter methodBody = new StringWriter();
			
			List<PropertyDescriptor> properties = getSetableProperties(c,true);

			String setterTpl = S(/*
			if(cfg.containsKey("{PROPNAME}")){
				{SETTERBODY}
			}*/);
			
			String simpleSetter = S(/*obj.{SETTER}( (PROPTYPE) cfg.get( "{PROPNAME}" ) );"*/);
			
			String contextSetter = S(/*obj.{SETTER}( (PROPTYPE) ctx.get( (String)cfg.get( "{PROPNAME}" ) ) );*/);
			
			for (PropertyDescriptor property : properties) {

				// TODO: implement different types of setters (reference, simple, enum)
				
				
				methodBody.append( S(/*if(cfg.hasProperty("{PROPNAME}")){
					if(cfg.isContextual( "{PROPNAME}" )){
						obj.{SETTER}( ctx.getProperty( (String)cfg.getProperty( "{PROPNAME}" ) ) );
					}else{
						obj.{SETTER}( ({PROPTYPE})cfg.getProperty( "{PROPNAME}" ) );
					}
				}*/).replace("{PROPNAME}", property.getName()).replace("{SETTER}", property.getName()).replace("{PROPTYPE}", property.getType())
				);
			}
			
			methods.append( S(/* 
			public static {CLASS} {METHOD}( Config cfg, Context ctx ){
				{CLASS} obj = new {CLASS}();
				{BODY}
			}
			*/).replace("{CLASS}", c.getSimpleName()).replace("{METHOD}", methodName).replace("{BODY}",methodBody.toString()) );
			
		}

		return S(
		/*
		 package web.dynamic.generated{
			
			#IMPORTS#
		  
		 	public class GXTComponentBuilder{
		   
		  		public static Component buildFromConfig( Config cfg ){ 
		  			if( !cfg.<String>has("xtype") ) return null;
		  			else return createFromXType( cfg.<String>get("xtype"), cfg); 
		  		}
		  	
		  		public static Component createFromXType( String xtype, Config cfg ){
		  			#XTYPES# 
		  		}
		  
		  		#BUILDER_METHODS# 
			} 
		}
		 */).replace("#IMPORTS#", imports.toString()).replace("#XTYPES#",
				xtypes.toString()).replace("#BUILDER_METHODS#",
				methods.toString());

	}

	//TODO: get types from pakage, reduce(follow the rabit hole), generate the code
	public List<Class<?>> reduceHeirarchy(List<Class<?>> types) {
		return null;
	}

	@SuppressWarnings("unchecked")
	public List<PropertyDescriptor> getSetableProperties(Class clazz,
			boolean searchHeirarchy) {

		List<PropertyDescriptor> properties = new ArrayList<PropertyDescriptor>();
		Method[] methods = clazz.getDeclaredMethods();
		for (Method method : methods) {
			if (method.getName().startsWith("set")
					&& method.getParameterTypes().length == 1) {
				PropertyDescriptor desc = new PropertyDescriptor();
				desc.setName(method.getName());
				desc.setType(method.getParameterTypes()[0].getCanonicalName());
				properties.add(desc);
			}
		}
		if (searchHeirarchy) {
			Class parent = clazz.getSuperclass();
			while (parent != null && parent != Object.class) {
				properties.addAll(getSetableProperties(parent, false));
				parent = parent.getSuperclass();
			}
		}

		return properties;
	}

	public static Class[] getClasses(String pckgname)
			throws ClassNotFoundException {
		ArrayList classes = new ArrayList();
		// Get a File object for the package
		File directory = null;
		try {
			directory = new File(Thread.currentThread().getContextClassLoader()
					.getResource('/' + pckgname.replace('.', '/')).getFile());
		} catch (NullPointerException x) {
			throw new ClassNotFoundException(pckgname
					+ " does not appear to be a valid package");
		}
		if (directory.exists()) {
			// Get the list of the files contained in the package
			String[] files = directory.list();
			for (int i = 0; i < files.length; i++) {
				// we are only interested in .class files
				if (files[i].endsWith(".class")) {
					// removes the .class extension
					classes.add(Class.forName(pckgname + '.'
							+ files[i].substring(0, files[i].length() - 6)));
				}
			}
		} else {
			throw new ClassNotFoundException(pckgname
					+ " does not appear to be a valid package");
		}
		Class[] classesA = new Class[classes.size()];
		classes.toArray(classesA);
		return classesA;
	}

	@Override
	public final void doGet(HttpServletRequest request, HttpServletResponse response) {
		
		Package p = Package.getPackage("com.extjs.gxt.ui.client");
		//p.
		
		
		List<Class<?>> types = new ArrayList<Class<?>>();
		types.add(FormPanel.class);
		//types.add( (Grid<ModelData>).class );
		
		
		Map<String,Object> data = new HashMap<String,Object>();
		data.put("xtype", FormPanel.class.getCanonicalName());
		
		data.put("properties", getSetableProperties( FormPanel.class, true ));
		//data.put("events", getEvents(FormPanel.class, true));

		BufferedWriter w;
		try {
			
			w = new BufferedWriter( new PrintWriter( response.getOutputStream() ) );
			
			GsonBuilder b = new GsonBuilder();
			
			Gson gson = b.create();
			
			w.write( generateBuilder( types ) );
			
			w.close();
			
		} catch (IOException e) {
			
			e.printStackTrace();
		}
		
	}

	@SuppressWarnings("unchecked")
	public List<PropertyDescriptor> getEvents(Class clazz,
			boolean searchHeirarchy) {

		List<PropertyDescriptor> events = new ArrayList<PropertyDescriptor>();

		String s = getSource(clazz);
		Pattern p = Pattern.compile(Pattern.quote("<dt><b>Events:</b></dt>")
				+ "(.*?)" + Pattern.quote("</dl>"), Pattern.DOTALL);
		Matcher m = p.matcher(s);

		if (m.find()) {
			String detail = m.toMatchResult().group(1);
			Pattern patEvt = Pattern.compile(Pattern.quote("<dd><b>") + "(.*?)"
					+ Pattern.quote("</b> : ") + "(.*?)"
					+ Pattern.quote("<br>"), Pattern.DOTALL);
			Matcher matEvt = patEvt.matcher(detail);
			while (matEvt.find()) {
				PropertyDescriptor desc = new PropertyDescriptor();
				desc.setName(matEvt.group(1));
				desc.setType(matEvt.group(2));
				events.add(desc);
			}
		}

		if (searchHeirarchy) {
			Class parent = clazz.getSuperclass();
			while (parent != null && parent != Object.class) {
				events.addAll(getEvents(parent, false));
				parent = parent.getSuperclass();
			}
		}

		return events;
	}

	public static String getSource(Class<?> clazz) {
		String name = clazz.getCanonicalName().replace('.', '/') + ".java";
		InputStream in = Class.class.getResourceAsStream('/' + name);
		return convertStreamToString(in, 0);
	}

	@Override
	public final void doPost(HttpServletRequest request,
			HttpServletResponse response) {

	}

	// From:
	// http://blog.efftinge.de/2008/10/multi-line-string-literals-in-java.html
	// Takes a comment (/**/) and turns everything inside the comment to a
	// string that is returned from S()
	public static String S() {
		StackTraceElement element = new RuntimeException().getStackTrace()[1];
		String name = element.getClassName().replace('.', '/') + ".java";
		InputStream in = Object.class.getResourceAsStream(name);
		if(in == null)
			in = Object.class.getResourceAsStream("/"+name);
		String s = convertStreamToString(in, element.getLineNumber());
		return s.substring(s.indexOf("/*") + 2, s.indexOf("*/"));
	}

	// From http://www.kodejava.org/examples/266.html
	private static String convertStreamToString(InputStream is, int lineNum) {
		/*
		 * To convert the InputStream to String we use the
		 * BufferedReader.readLine() method. We iterate until the BufferedReader
		 * return null which means there's no more data to read. Each line will
		 * appended to a StringBuilder and returned as String.
		 */
		BufferedReader reader = new BufferedReader(new InputStreamReader(is));
		StringBuilder sb = new StringBuilder();

		String line = null;
		int i = 1;
		try {
			while ((line = reader.readLine()) != null) {
				if (i++ >= lineNum) {
					sb.append(line + "\n");
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		return sb.toString();
	}
	
}
